﻿# -*- coding: utf-8 -*-


# Copyright 2009 Larry Resnik, Lasfraka-Elixir

#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import division

import os
import pyglet
from pyglet.gl import (glEnable, glBlendFunc, glClearColor, glLoadIdentity,
    glScalef, glColor3f, glRectf, glColor4f, GL_SRC_ALPHA, GL_BLEND,
    GL_ONE_MINUS_SRC_ALPHA)
from pyglet.window import key
import gamescene
import mainmenu
from config import Settings
from sound_handler.silent_handler import SilentHandler
from sound_handler.audiere_handler import AudiereHandler
from audiere import open_device as open_audiere_device


__version__ = '1b.4.6'

# This is the true framerate to hit.
SKIP_TICKS = 1.0 / 60.0
# This is how many update iterations may be made when we're behind due to lag.
MAX_FRAMESKIP = 10


def invert(num, max):
    """Returns what the passed number would be on the opposite end of max.

    Used for formulas that need to decrement by doing an increment.
    """
    return num + max - (num + num)


class SceneManager(object):
    """This class manages all scenes.

    A scene is any component of the program that can be displayed all at once.
    Switch scenes using pyglet.clock.schedule_once(switch_scene, 0.0, 'scene')
    such that 'scene' is the desired scene. Unschedule and schedule update
    methods as needed.
    """

    def __init__(self, window, bgm_player, settings, filePath=''):
        self.window = window
        self.bgm_player = bgm_player
        self.settings = settings
        self.filePath = filePath
        # Note: If there isn't a separator on the end of mediaPath, the
        # resources' names would be blended at the end. Also, index -1 on a
        # null string is an IndexError.
        if len(filePath) > 0 and filePath[-1] != os.sep:
            filePath = ''.join((filePath, os.sep))
        # Now make the path look good for the OS running this.
        self.filePath = os.path.abspath(filePath)
        self.sound_path = os.path.join(self.filePath,
            os.path.normpath(u'res/snd/'))

        # Set window and class set-up stuff.
        # The following 2 commands allow transparency in PNGs.
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glClearColor(0.0, 0.3, 0.0, 0.0)
        pyglet.resource.path = ['res/tiles', 'res/sprites', 'res/bullets',
                                'res/special']
        pyglet.resource.reindex()
        try:
            window.set_icon(pyglet.image.load(os.path.join(filePath,
                os.path.abspath(u'res/special/icon.png'))))
        except IOError:
            pass
        window.set_mouse_visible(False)
        self.openSounds = {}
        self.audiere_device = open_audiere_device()
        self.set_bgm('menu.ogg')

        # These are used during fade-out/in transitions.
        self.fadeTime = 0
        self.fadeDuration = 20
        self.fadeOpacity = 0.0
        self.fadeFunction = None
        # If it's False, then it's a fade-in.
        self.isFadeout = True

        # These are used for the framerate limiting.
        self.fpsLabel = pyglet.text.Label('0', font_size=12, x=290, y=10,
            bold=True, color=(255, 0, 0, 255 * settings.properties['fps']))
        self.thisGameTick = 0.0
        self.nextGameTick = self.thisGameTick
        self.previous_fps_update = self.thisGameTick

        self.key_assign = settings.key_watch_manager

        # Set scenes.
        self._preload_scenes()
        self.currentScene = mainmenu.MainMenuScene(self)
        self.savedScene = None
        # This is for Scene transitions. It's set before a fade-out and cleared
        # during the fade-in.
        self.nextSceneName = ''
        self.updating = True

        window.push_handlers(self.key_assign)
        window.push_handlers(self)
        window.push_handlers(self.currentScene)
        pyglet.clock.schedule_interval(self.update,
            1.0 / self.settings.framerate)

    def enable_fps(self, bool):
        """Changes the FPS's visibility."""
        self.fpsLabel.color = (self.fpsLabel.color[0], self.fpsLabel.color[1],
            self.fpsLabel.color[2], 255 * bool)
        self.settings.properties['fps'] = bool

    def _preload_scenes(self):
        """Preloads all scenes, but not currentScene and savedScene.

        Preloaded scenes may or may not be in the last release. It depends
        on the effectiveness of threads.
        """

        if not self.settings.preload_scenes:
            return
        self.gameScene = gamescene.GameScene(self)
        self.weaponScene = WeaponScene(self)
        self.mainMenuScene = mainmenu.MainMenuScene(self)

    def _scene_swap(self, scene):
        self.currentScene = scene
        self.window.pop_handlers()
        self.window.push_handlers(scene)

    def _load_sound(self, filename):
        if filename in self.openSounds:
            return

        full_path = os.path.join(self.sound_path, filename)
        mySound = AudiereHandler(self.audiere_device, full_path,
            self.settings.properties['sfx_volume'])

        # This statement turns the actual filename into a key whose value
        # is the mySound object. The dict is added to openSounds. Note that
        # calling dict() in a more understandable fashion turns the key into
        # 'filename' consistently.
        self.openSounds.update(dict([[filename, mySound]]))
        return mySound

    def play_sound(self, filename):
        # See if the file was already loaded. If True, play that.
        if filename not in self.openSounds:
            self._load_sound(filename)
        if self.openSounds[filename] is not None:
            self.openSounds[filename].force_start_sound()

    def set_bgm(self, loop_file=None, intro_file=None):
        """Sets up the BGM Player variable to own and play the passed files.

        The BGM Player is not told to play within this method. Whatever was in
        the BGM Player previously will be carelessly thrown out.
        If both files are left as None, the items in bgm_names is used. This is
        prefered for just reloading a level after death for example.
        Note that bgm_names is a tuple of (loop_name, intro_name).
        """
        loop_file = os.path.join(self.sound_path, loop_file)
        if intro_file:
            intro_file = os.path.join(self.sound_path, intro_file)
        self.bgm_player.set_stream(self.audiere_device, loop_file, intro_file)
        self.reset_bgm_volume()

    def reset_bgm_volume(self):
        """Sets the volume for the bgm."""
        self.bgm_player.set_volume(self.settings.properties['bgm_volume'])

    def switch_scene_preloaded(self, dt, request=''):
        if self.fadeFunction is not None or not self.nextSceneName:
            # During a fade, do not allowing scene switching.
            return
        request = self.nextSceneName
        if request == 'game':
            self._scene_swap(self.gameScene)
        elif request == 'weapon':
            self._scene_swap(self.weaponScene)
        elif request == 'mainmenu':
            self._scene_swap(self.mainMenuScene)
        else:
            print('Invalid scene: %s' % request)
        self.nextSceneName = ''
        self.request_fade(0.0, False, self.start_updating)

    def switch_scene(self, dt, request=''):
        if self.fadeFunction is not None or not self.nextSceneName:
            return
        if not request:
            request = self.nextSceneName
        if request == 'game':
            # MainMenuScene and WeaponScene may go to GameScene.
            # Restore GameScene and forget the rest.
            if not self.savedScene:
                self.savedScene = gamescene.GameScene(self)
            self.currentScene = self.savedScene
            self._scene_swap(self.currentScene)
        elif request == 'weapon':
            # Only GameScene will go to WeaponScene. Store GameScene for now.
            self.savedScene = self.currentScene
            self._scene_swap(WeaponScene(self))
        elif request == 'mainmenu':
            # Only GameScene may go back to MainMenuScene. And by then, we
            # don't want to save the game anymore.
            savedScene.cleanup()
            self.savedScene = None
            self._scene_swap(mainmenu.MainMenuScene(self))
        else:
            print('Invalid scene: %s' % request)
        self.nextSceneName = ''
        self.request_fade(0.0, False, self.start_updating)

    def fading_scene_transition(self, dt=0.0, nextScene=''):
        """Fades from the current Scene to the next.

        Updating is disabled during the transition.
        """
        if nextScene:
            self.nextSceneName = nextScene
        self.updating = False
        if self.settings.preload_scenes:
            self.request_fade(0.0, True, self.switch_scene_preloaded)
        else:
            self.request_fade(0.0, True, self.switch_scene)

    def start_updating(self, dt=0.0):
        """Sets updating to True."""
#        if self.updating:
#            return
        # Without this statement, the game would try to make up for lost time.
        self.thisGameTick = self.nextGameTick
        self.updating = True

    def _update_fade(self, dt):
        self.fadeTime += 1
        if self.isFadeout:
            self.fadeOpacity = self.fadeTime / self.fadeDuration
        else:
            self.fadeOpacity = invert(self.fadeTime, self.fadeDuration) / \
                self.fadeDuration
        if self.fadeTime == self.fadeDuration:
            pyglet.clock.unschedule(self._update_fade)
            pyglet.clock.schedule_once(self.fadeFunction, 0.0)
            if self.isFadeout:
                self.fadeOpacity = 1.0
            else:
                self.fadeOpacity = 0.0
            self.fadeFunction = None

    def request_fade(self, dt, is_fadeout, finisher_function, duration=20):
        """Initiates a full-screen fade then calls finisher_function.

        fadeout is a boolean which says if the fade effect is a fade-in or a
        fade-out effect.
        finisher_function is called when the fade effect has completed. That
        function may need to call request_fade again.
        """
        self.fadeTime = 0
        self.isFadeout = is_fadeout
        self.fadeFunction = finisher_function
        self.fadeDuration = duration
        pyglet.clock.schedule_interval(self._update_fade,
            1.0 / self.settings.framerate)

    def on_exit(self):
        if self.bgmPlayer.playing:
            self.bgmPlayer.pause()
        pyglet.clock.unschedule(self._update_fps_label)

    def on_draw(self):
        # For some reason, py2exe makes sprites affected by the fade, so this
        # is necessary at all times.
        glColor3f(1.0, 1.0, 1.0)
        glLoadIdentity()
        glScalef(self.settings.properties['scale'] / 100,
                 self.settings.properties['scale'] / 100, 1.0)
        if self.fadeOpacity >= 0.01:
            # This does a full-screen, gradual fade-in/out.
            glColor4f(0.0, 0.0, 0.0, self.fadeOpacity)
            glRectf(0, 0, 320, 240)
            # This gives back the color to the Sprites.
#            glColor3f(1.0, 1.0, 1.0)
        self.fpsLabel.draw()

    def update(self, dt=0.0):
        """Catch up with the desired frames per second.

        The game ticks are iterated multiple times if needed.
        """

        self.thisGameTick += dt
        if self.thisGameTick - self.previous_fps_update >= 0.6:
            self.fpsLabel.text = '%i' % pyglet.clock.get_fps()
            self.previous_fps_update = self.thisGameTick

        if self.thisGameTick <= self.nextGameTick or not self.updating:
            return
        self.key_assign.update()

        # Doing this check in the loop is killer because Python hates when
        # you alter a loop's iterator.
        loops = 0
        while loops < MAX_FRAMESKIP:
            self.currentScene.iterate_scene(self.key_assign)
            self.nextGameTick += SKIP_TICKS
            # Crude hack to only allow multi-updates during gameplay.
            # Otherwise buttons are interpretted as being pressed multiple
            # times. I should probably not update multiple times anymore.
            if self.thisGameTick <= self.nextGameTick or \
                self.currentScene != 'game':
                break
            loops += 1


class WeaponScene(object):
    """This is a scene for the weapon selection screen."""
    def __init__(self, manager):
        self.manager = manager
        self.label = pyglet.text.Label('This is the weapon screen. Really.',
                                       x=80, y=60)

    def on_key_press(self, symbol, modifiers):
        # Hell, I'll let everything work for now.
        if symbol:
            pyglet.clock.schedule_once(self.manager.switch_scene, 0.0, 'game')

    def on_draw(self):
        glLoadIdentity()
        glScalef(self.manager.settings.properties['scale'] / 100,
                 self.manager.settings.properties['scale'] / 100, 1.0)
        self.label.draw()

    def iterate_scene(self, keys):
        """Does one update cycle for this Scene."""
        pass


def run_game(bgm_player, filePath='', framerate=60.0):
    settings = Settings()
    settings.read()
    if settings.properties['fullscreen']:
        window = pyglet.window.Window(resizable=False, caption='MegaMari X',
            fullscreen=True, vsync=settings.properties['vsync'])
        settings.properties['scale'] = 100 * min(window.width / 320,
            window.height / 240)
    else:
        window = pyglet.window.Window(
            int(320 * settings.properties['scale'] / 100),
            int(240 * settings.properties['scale'] / 100), resizable=False,
            caption='MegaMari X', vsync=settings.properties['vsync'])
    window.set_visible()
    SceneManager(window, bgm_player, settings, filePath)
    pyglet.app.run()
